﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Db4objects.Db4o;
using Db4objects.Db4o.Linq;
using Db4objects.Db4o.Config;
using Db4objects.Db4o.CS.Config;

namespace SpellCorrector
{
    public class SpellCorrector
    {
        
        IObjectContainer _Client;
        BkTreeNode _InMemoryTree;
        /// <summary>
        /// A BK tree in memory. The tree node is the first node in the tree
        /// </summary>
        BkTreeNode InMemoryBKTree
        {
            get
            {
                if (_InMemoryTree == null)
                    BuildImMemoryTree();
                return _InMemoryTree;
            }
            
        }
        public int NumberOfWordsInTheDictionary
        {
            get
            {
                var query = from Node n in _Client select n;
                return query.Count();
            }
        }

        public SpellCorrector()
        {
            //synchronize access to server for multi threaded applications
            lock (Server)
            {
                _Client = Server.OpenClient();
                
            }
        }
         ~SpellCorrector()
        {
            _Client.Close();
        }

         
        /// <summary>
        /// 
        /// </summary>
        /// <param name="word"></param>
        /// <param name="distance">The maximum distance between the entered word and the suggested corrections</param>
        /// <returns></returns>
        public List<string> CorrectWord(string word, int distance,int maxNumberOfResults)
        {
            //the spell corrector is case insensitive
            word = word.ToLower();
            word = RemoveSpecialCharactersAndNumbers(word);
            List<BkTreeNode> corrections = CorrectWordUsingInMemoryBkTree(word, distance);
            List<string> sortedCorrections = OrderSuggestedCorrectionsUsingBayesAlgorithmAndLevensteinDistance(word, corrections,maxNumberOfResults);
            return sortedCorrections;
        }

        private List<BkTreeNode> CorrectWordUsingInMemoryBkTree(string word, int maxDistance)
        {
            List<BkTreeNode> SuggestedCorrections = new List<BkTreeNode>();
            SearchInMemoryNodeForPossibleCorrections(word, InMemoryBKTree, maxDistance, SuggestedCorrections);

            return SuggestedCorrections;
        }

        private void SearchInMemoryNodeForPossibleCorrections(string word, BkTreeNode inMemoryNode, int maxLevenshteinDistanceRange,List<BkTreeNode> possibleCorrections)
        {
            int distance = Node.CalculateLevenshteinDistance(inMemoryNode.Word, word);
            if (distance <= maxLevenshteinDistanceRange)
            {

                possibleCorrections.Add(inMemoryNode);
            }
            int upperBound = distance + maxLevenshteinDistanceRange;
            int lowerBound = distance - maxLevenshteinDistanceRange;
            //get children whose distance are in the range d+n and d-n
            List<BkTreeNode> childrenAtRange = inMemoryNode.GetNodesAtDistanceRange(upperBound, lowerBound);
            foreach (BkTreeNode nodeInstance in childrenAtRange)
            {
                SearchInMemoryNodeForPossibleCorrections(word, nodeInstance, maxLevenshteinDistanceRange, possibleCorrections);
            }
        }
        /// <summary>
        /// Add a correct word to the tree
        /// </summary>
        /// <param name="word"></param>
        public void AddWordToDictionary(string word)
        {
            //the spell corrector is case insensitive
            word = word.ToLower();
            word = RemoveSpecialCharactersAndNumbers(word);
            
                
                var previouslyFoundWordQuery = from Node n in _Client where n.Word == word select n;
                if (previouslyFoundWordQuery.Count() > 0)
                {
                    //this word has been seen before
                    Node nodeInstance = previouslyFoundWordQuery.Single();
                    nodeInstance.NumberofTimesSeenBefore++;
                    _Client.Store(nodeInstance);
                    _Client.Commit();
                    return;
                }
                //the word has not been seen before and it is not a head word
                
                Node newNode = new Node();
                
                newNode.Word = word;
                newNode.NumberofTimesSeenBefore = 1;
                _Client.Store(newNode);
                _Client.Commit();
            
        }
        /// <summary>
        /// For a fast search, build the tree in memory instead of storing it in the database or a file structure
        /// </summary>
        public void BuildImMemoryTree()
        {
            _InMemoryTree = null;
            var query = from Node n in _Client select n;
            foreach (Node n in query)
            {
                //initialize head node
                if (_InMemoryTree == null)
                {
                    _InMemoryTree = new BkTreeNode(n);
                }
                else
                {
                    BkTreeNode childNode = new BkTreeNode(n);
                    InMemoryBKTree.AddChildNode(childNode);
                }
            }
        }
        private string RemoveSpecialCharactersAndNumbers(string word)
        {
            char[] unwantedCharacters = { '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '-', '+', '=', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '\n', '\t','\r', ' ', '.', ';', ',', '?', '"', '<', '>', ':','\\','/','|','[',']','{','}' };
            word=word.Trim(unwantedCharacters);
            word = word.Replace("'s", "");
            
            return word;
        }


        public void DeleteWordFromDictionary(string Word)
        {
            var query = from Node n in _Client where n.Word == Word select n;
            if (query.Count() == 0)
                return;
            Node nodeToBeDeleted = query.Single();
            _Client.Delete(nodeToBeDeleted);
            _Client.Commit();
        }
        /// <summary>
        /// Modifies a word in the dictionary entered by error. If the wrong word doesn't exist it adds the correct word to the dictionary
        /// </summary>
        /// <param name="wrongWord"></param>
        /// <param name="correctWord"></param>
        public void CorrectWordInDictionary(string wrongWord, string correctWord)
        {
            var query = from Node n in _Client where n.Word == wrongWord select n;
            if (query.Count() == 0)
            {
                Node newNode = new Node();
                newNode.Word = correctWord;
                newNode.NumberofTimesSeenBefore = 1;
                _Client.Store(newNode);
                _Client.Commit();
                return;    
            }
            Node nodeToModify = query.Single();
            nodeToModify.Word = correctWord;
            _Client.Store(nodeToModify);
            _Client.Commit();
        }
      

        private List<string> OrderSuggestedCorrectionsUsingBayesAlgorithmAndLevensteinDistance(string wordToBeCorrected, List<BkTreeNode> suggestedCorrections,int maxNumberOfResults)
        {
            SortedList<double, string> sortedSuggestions = new SortedList<double, string>();
            double totalNumberOfNodes = (from Node n in _Client select n).Count();
            foreach (Node n in suggestedCorrections)
            {
                double levensteinDistance = Node.CalculateLevenshteinDistance(wordToBeCorrected, n.Word);
                if (levensteinDistance == 0)
                    //to avoid division by zero set the distance to a very small value but not zero
                    levensteinDistance = 0.00000000000001;
                double ProbabilityOfCorrectionBeingTrue = 1 / levensteinDistance;
                double weight = ProbabilityOfCorrectionBeingTrue * n.NumberofTimesSeenBefore;
                if (sortedSuggestions.Count == maxNumberOfResults)
                    sortedSuggestions.RemoveAt(0);
                //a sorted list doesn't allow duplicate keys
                while (sortedSuggestions.Keys.Contains(weight))
                {
                    //add a small value that doesn't affect the sorting of the results but prevents key duplication
                    weight=weight +0.00000001;
                }
                sortedSuggestions.Add(weight, n.Word);
            }
            //the list is sorted ascendingly. Sort it descendingly so that the highest rank words are at the top
            return sortedSuggestions.Values.Reverse().ToList() ;
        }

        
        
        private static IObjectServer _Server;
        /// <summary>
        /// For multithreading purpose, use a client server model with single static server and multiple clients
        /// </summary>
        private static IObjectServer Server
        {
            get
            {

                if (_Server == null)
                {

                    IServerConfiguration serverConfiguraiton = Db4objects.Db4o.CS.Db4oClientServer.NewServerConfiguration();
                    //set indexes                    
                    serverConfiguraiton.Common.ObjectClass(typeof(Node)).ObjectField("Word").Indexed(true);
                    
                    _Server = Db4objects.Db4o.CS.Db4oClientServer.OpenServer(serverConfiguraiton, "Dictionary", 0);

                }
                return _Server;

            }
        }
       

    }
}
